/*
 * RED5 Open Source Flash Server - http://code.google.com/p/red5/
 * 
 * Copyright 2006-2014 by respective authors (see below). All rights reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.red5.server.util;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Enumeration;
import java.util.Random;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Generic file utility containing useful file or directory
 * manipulation functions.
 * 
 * @author Paul Gregoire (mondain@gmail.com)
 * @author Dominick Accattato (daccattato@gmail.com)
 */
public class FileUtil {

	private static Logger log = LoggerFactory.getLogger(FileUtil.class);

	public static void copyFile(File source, File dest) throws IOException {
		log.debug("Copy from {} to {}", source.getAbsoluteFile(), dest.getAbsoluteFile());
		FileInputStream fi = new FileInputStream(source);
		FileChannel fic = fi.getChannel();
		MappedByteBuffer mbuf = fic.map(FileChannel.MapMode.READ_ONLY, 0, source.length());
		fic.close();
		fi.close();
		fi = null;

		// ensure the destination directory exists
		if (!dest.exists()) {
			String destPath = dest.getPath();
			log.debug("Destination path: {}", destPath);
			String destDir = destPath.substring(0, destPath.lastIndexOf(File.separatorChar));
			log.debug("Destination dir: {}", destDir);
			File dir = new File(destDir);
			if (!dir.exists()) {
				if (dir.mkdirs()) {
					log.debug("Directory created");
				} else {
					log.warn("Directory not created");
				}
			}
			dir = null;
		}

		FileOutputStream fo = new FileOutputStream(dest);
		FileChannel foc = fo.getChannel();
		foc.write(mbuf);
		foc.close();
		fo.close();
		fo = null;

		mbuf.clear();
		mbuf = null;
	}

	public static void copyFile(String source, String dest) throws IOException {
		copyFile(new File(source), new File(dest));
	}

	public static void moveFile(String source, String dest) throws IOException {
		copyFile(source, dest);
		File src = new File(source);
		if (src.exists() && src.canRead()) {
			if (src.delete()) {
				log.debug("Source file was deleted");
			} else {
				log.debug("Source file was not deleted, the file will be deleted on exit");
				src.deleteOnExit();
			}
		} else {
			log.warn("Source file could not be accessed for removal");
		}
		src = null;
	}

	/**
	 * Deletes a directory and its contents. This will fail if there are any
	 * file locks or if the directory cannot be emptied.
	 * 
	 * @param directory directory to delete
	 * @throws IOException if directory cannot be deleted
	 * @return true if directory was successfully deleted; false if directory
	 *  did not exist
	 */
	public static boolean deleteDirectory(String directory) throws IOException {
		return deleteDirectory(directory, false);
	}

	/**
	 * Deletes a directory and its contents. This will fail if there are any
	 * file locks or if the directory cannot be emptied.
	 * 
	 * @param directory directory to delete
	 * @param useOSNativeDelete flag to signify use of operating system delete function
	 * @throws IOException if directory cannot be deleted
	 * @return true if directory was successfully deleted; false if directory
	 *  did not exist
	 */
	public static boolean deleteDirectory(String directory, boolean useOSNativeDelete) throws IOException {
		boolean result = false;
		if (!useOSNativeDelete) {
			File dir = new File(directory);
			// first all files have to be cleared out
			for (File file : dir.listFiles()) {
				if (file.delete()) {
					log.debug("{} was deleted", file.getName());
				} else {
					log.debug("{} was not deleted", file.getName());
					file.deleteOnExit();
				}
				file = null;
			}
			// not you may remove the dir
			if (dir.delete()) {
				log.debug("Directory was deleted");
				result = true;
			} else {
				log.debug("Directory was not deleted, it may be deleted on exit");
				dir.deleteOnExit();
			}
			dir = null;
		} else {
			Process p = null;
			Thread std = null;
			try {
				Runtime runTime = Runtime.getRuntime();
				log.debug("Execute runtime");
				//determine file system type
				if (File.separatorChar == '\\') {
					//we are windows
					p = runTime.exec("CMD /D /C \"RMDIR /Q /S " + directory.replace('/', '\\') + "\"");
				} else {
					//we are unix variant 
					p = runTime.exec("rm -rf " + directory.replace('\\', File.separatorChar));
				}
				// observe std out
				std = stdOut(p);
				// wait for the observer threads to finish
				while (std.isAlive()) {
					try {
						Thread.sleep(250);
					} catch (Exception e) {
					}
				}
				log.debug("Process threads wait exited");
				result = true;
			} catch (Exception e) {
				log.error("Error running delete script", e);
			} finally {
				if (null != p) {
					log.debug("Destroying process");
					p.destroy();
					p = null;
				}
				std = null;
			}
		}
		return result;
	}

	/**
	 * Rename a file natively; using REN on Windows and mv on *nix.
	 * 
	 * @param from old name
	 * @param to new name
	 */
	public static void rename(String from, String to) {
		Process p = null;
		Thread std = null;
		try {
			Runtime runTime = Runtime.getRuntime();
			log.debug("Execute runtime");
			//determine file system type
			if (File.separatorChar == '\\') {
				//we are windows
				p = runTime.exec("CMD /D /C \"REN " + from + ' ' + to + "\"");
			} else {
				//we are unix variant 
				p = runTime.exec("mv -f " + from + ' ' + to);
			}
			// observe std out
			std = stdOut(p);
			// wait for the observer threads to finish
			while (std.isAlive()) {
				try {
					Thread.sleep(250);
				} catch (Exception e) {
				}
			}
			log.debug("Process threads wait exited");
		} catch (Exception e) {
			log.error("Error running delete script", e);
		} finally {
			if (null != p) {
				log.debug("Destroying process");
				p.destroy();
				p = null;
				std = null;
			}
		}
	}

	/**
	 * Special method for capture of StdOut.
	 * 
	 * @return stdOut thread
	 */
	private final static Thread stdOut(final Process p) {
		final byte[] empty = new byte[128];
		for (int b = 0; b < empty.length; b++) {
			empty[b] = (byte) 0;
		}
		Thread std = new Thread() {
			public void run() {
				StringBuilder sb = new StringBuilder(1024);
				byte[] buf = new byte[128];
				BufferedInputStream bis = new BufferedInputStream(p.getInputStream());
				log.debug("Process output:");
				try {
					while (bis.read(buf) != -1) {
						sb.append(new String(buf).trim());
						// clear buffer
						System.arraycopy(empty, 0, buf, 0, buf.length);
					}
					log.debug(sb.toString());
					bis.close();
				} catch (Exception e) {
					log.error("{}", e);
				}
			}
		};
		std.setDaemon(true);
		std.start();
		return std;
	}

	/**
	 * Create a directory.
	 * 
	 * @param directory directory to make
	 * @return whether a new directory was made
	 * @throws IOException if directory does not already exist or cannot be made
	 */
	public static boolean makeDirectory(String directory) throws IOException {
		return makeDirectory(directory, false);
	}

	/**
	 * Create a directory. The parent directories will be created if
	 * <i>createParents</i> is passed as true.
	 * 
	 * @param directory directory
	 * @param createParents whether to create all parents
	 * @return true if directory was created; false if it already existed
	 * @throws IOException if we cannot create directory
	 * 
	 */
	public static boolean makeDirectory(String directory, boolean createParents) throws IOException {
		boolean created = false;
		File dir = new File(directory);
		if (createParents) {
			created = dir.mkdirs();
			if (created) {
				log.debug("Directory created: {}", dir.getAbsolutePath());
			} else {
				log.debug("Directory was not created: {}", dir.getAbsolutePath());
			}
		} else {
			created = dir.mkdir();
			if (created) {
				log.debug("Directory created: {}", dir.getAbsolutePath());
			} else {
				log.debug("Directory was not created: {}", dir.getAbsolutePath());
			}
		}
		dir = null;
		return created;
	}

	/**
	 * Unzips a war file to an application located under the webapps directory
	 * 
	 * @param compressedFileName The String name of the war file
	 * @param destinationDir The destination directory, ie: webapps
	 */
	public static void unzip(String compressedFileName, String destinationDir) {

		//strip everything except the applications name
		String dirName = null;

		// checks to see if there is a dash "-" in the filename of the war. 
		String applicationName = compressedFileName.substring(compressedFileName.lastIndexOf("/"));

		int dashIndex = applicationName.indexOf('-');
		if (dashIndex != -1) {
			//strip everything except the applications name
			dirName = compressedFileName.substring(0, dashIndex);
		} else {
			//grab every char up to the last '.'
			dirName = compressedFileName.substring(0, compressedFileName.lastIndexOf('.'));
		}

		log.debug("Directory: {}", dirName);
		//String tmpDir = System.getProperty("java.io.tmpdir");
		File zipDir = new File(compressedFileName);
		File parent = zipDir.getParentFile();
		log.debug("Parent: {}", (parent != null ? parent.getName() : null));
		//File tmpDir = new File(System.getProperty("java.io.tmpdir"), dirName);
		File tmpDir = new File(destinationDir);

		// make the war directory
		log.debug("Making directory: {}", tmpDir.mkdirs());
		ZipFile zf = null;
		try {
			zf = new ZipFile(compressedFileName);
			Enumeration<?> e = zf.entries();
			while (e.hasMoreElements()) {
				ZipEntry ze = (ZipEntry) e.nextElement();
				log.debug("Unzipping {}", ze.getName());
				if (ze.isDirectory()) {
					log.debug("is a directory");
					File dir = new File(tmpDir + "/" + ze.getName());
					Boolean tmp = dir.mkdir();
					log.debug("{}", tmp);
					continue;
				}

				// checks to see if a zipEntry contains a path
				// i.e. ze.getName() == "META-INF/MANIFEST.MF"
				// if this case is true, then we create the path first
				if (ze.getName().lastIndexOf("/") != -1) {
					String zipName = ze.getName();
					String zipDirStructure = zipName.substring(0, zipName.lastIndexOf("/"));
					File completeDirectory = new File(tmpDir + "/" + zipDirStructure);
					if (!completeDirectory.exists()) {
						if (!completeDirectory.mkdirs()) {
							log.error("could not create complete directory structure");
						}
					}
				}

				// creates the file
				FileOutputStream fout = new FileOutputStream(tmpDir + "/" + ze.getName());
				InputStream in = zf.getInputStream(ze);
				copy(in, fout);
				in.close();
				fout.close();
			}
			e = null;
		} catch (IOException e) {
			log.error("Errored unzipping", e);
			//log.warn("Exception {}", e);
		} finally {
			if (zf != null) {
				try {
					zf.close();
				} catch (IOException e) {
				}
			}
		}
	}

	public static void copy(InputStream in, OutputStream out) throws IOException {
		synchronized (in) {
			synchronized (out) {
				byte[] buffer = new byte[256];
				while (true) {
					int bytesRead = in.read(buffer);
					if (bytesRead == -1)
						break;
					out.write(buffer, 0, bytesRead);
				}
			}
		}
	}

	/**
	 * Unzips a given archive to a specified destination directory.
	 * 
	 * @param compressedFileName
	 * @param destinationDir
	 */
	//	public static void unzip(String compressedFileName, String destinationDir) {
	//		log.debug("Unzip - file: {} destination: {}", compressedFileName, destinationDir);
	//		try {
	//			final int BUFFER = 2048;
	//			BufferedOutputStream dest = null;
	//			FileInputStream fis = new FileInputStream(compressedFileName);
	//			CheckedInputStream checksum = new CheckedInputStream(fis,
	//					new Adler32());
	//			ZipInputStream zis = new ZipInputStream(new BufferedInputStream(
	//					checksum));
	//			ZipEntry entry;
	//			while ((entry = zis.getNextEntry()) != null) {
	//				log.debug("Extracting: {}", entry);
	//				String name = entry.getName();
	//				int count;
	//				byte data[] = new byte[BUFFER];
	//				// write the files to the disk
	//				File destFile = new File(destinationDir, name);
	//				log.debug("Absolute path: {}", destFile.getAbsolutePath());
	//				//create dirs as needed, look for file extension to determine type
	//				if (entry.isDirectory()) {
	//					log.debug("Entry is detected as a directory");
	//					if (destFile.mkdirs()) {
	//						log.debug("Directory created: {}", destFile.getName());
	//					} else {
	//						log.warn("Directory was not created: {}", destFile.getName());
	//					}
	//					destFile = null;
	//					continue;
	//				}				
	//
	//				FileOutputStream fos = new FileOutputStream(destFile);
	//				dest = new BufferedOutputStream(fos, BUFFER);
	//				while ((count = zis.read(data, 0, BUFFER)) != -1) {
	//					dest.write(data, 0, count);
	//				}
	//				dest.flush();
	//				dest.close();
	//				destFile = null;
	//			}
	//			zis.close();
	//			log.debug("Checksum: {}", checksum.getChecksum().getValue());
	//		} catch (Exception e) {
	//			log.error("Error unzipping {}", compressedFileName, e);
	//			log.warn("Exception {}", e);
	//		}
	//
	//	}

	/**
	 * Quick-n-dirty directory formatting to support launching in windows, specifically from ant.
	 * @param absWebappsPath abs webapps path
	 * @param contextDirName conext directory name
	 * @return full path
	 */
	public static String formatPath(String absWebappsPath, String contextDirName) {
		StringBuilder path = new StringBuilder(absWebappsPath.length() + contextDirName.length());
		path.append(absWebappsPath);
		if (log.isTraceEnabled()) {
			log.trace("Path start: {}", path.toString());
		}
		int idx = -1;
		if (File.separatorChar != '/') {
			while ((idx = path.indexOf(File.separator)) != -1) {
				path.deleteCharAt(idx);
				path.insert(idx, '/');
			}
		}
		if (log.isTraceEnabled()) {
			log.trace("Path step 1: {}", path.toString());
		}
		//remove any './'
		if ((idx = path.indexOf("./")) != -1) {
			path.delete(idx, idx + 2);
		}
		if (log.isTraceEnabled()) {
			log.trace("Path step 2: {}", path.toString());
		}
		//add / to base path if one doesnt exist
		if (path.charAt(path.length() - 1) != '/') {
			path.append('/');
		}
		if (log.isTraceEnabled()) {
			log.trace("Path step 3: {}", path.toString());
		}
		//remove the / from the beginning of the context dir
		if (contextDirName.charAt(0) == '/' && path.charAt(path.length() - 1) == '/') {
			path.append(contextDirName.substring(1));
		} else {
			path.append(contextDirName);
		}
		if (log.isTraceEnabled()) {
			log.trace("Path step 4: {}", path.toString());
		}
		return path.toString();
	}

	/**
	 * Generates a custom name containing numbers and an underscore ex. 282818_00023.
	 * The name contains current seconds and a random number component.
	 * 
	 * @return custom name
	 */
	public static String generateCustomName() {
		Random random = new Random();
		StringBuilder sb = new StringBuilder();
		sb.append(PropertyConverter.getCurrentTimeSeconds());
		sb.append('_');
		int i = random.nextInt(99999);
		if (i < 10) {
			sb.append("0000");
		} else if (i < 100) {
			sb.append("000");
		} else if (i < 1000) {
			sb.append("00");
		} else if (i < 10000) {
			sb.append('0');
		}
		sb.append(i);
		return sb.toString();
	}

	/**
	 * Reads all the bytes of a given file into an array. If the file size exceeds Integer.MAX_VALUE, it will
	 * be truncated.
	 * 
	 * @param localSwfFile
	 * @return file bytes
	 */
	public static byte[] readAsByteArray(File localSwfFile) {
		byte[] fileBytes = new byte[(int) localSwfFile.length()];
		byte[] b = new byte[1];
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(localSwfFile);
			for (int i = 0; i < Integer.MAX_VALUE; i++) {
				if (fis.read(b) != -1) {
					fileBytes[i] = b[0];
				} else {
					break;
				}
			}
		} catch (IOException e) {
			log.warn("Exception reading file bytes", e);
		} finally {
			if (fis != null) {
				try {
					fis.close();
				} catch (IOException e) {
				}
			}
		}
		return fileBytes;
	}

}
